package org.erikaredmark.monkeyshines;
import java.awt.Graphics2D;
import java.util.ArrayList;
import java.util.List;
import org.erikaredmark.monkeyshines.resource.WorldResource;
import org.erikaredmark.monkeyshines.tiles.CommonTile;
import org.erikaredmark.monkeyshines.tiles.TileType;
/**
*
* Represents an n by m tile map, typically maxed out at around {@code GameConstants.TILES_IN_ROW by GameConstants.TILES_IN_COL} for use
* with a full level.
* <p/>
* Provides an easy abstraction for setting tiles via either mouse position (x,y) or row, column position. This class is backed by a single
* dimensional array.
* <p/>
* This class is not thread safe. When used by the game engine for playing, it should not be modified. Only in the editor context should it.
*
* @author Erika Redmark
*
*/
public class TileMap {
/**
*
* Creates a new tilemap at the given number of rows and columns.
* <p/>
* All tiles are initialised to the empty tilenot null)
*
* @param rows
* rows in the tilemap
*
* @param cols
* cols in the tilemap
*
*/
public TileMap(final int rows, final int cols) {
this.rows = rows;
this.cols = cols;
final int totalSize = rows * cols;
map = new TileType[totalSize];
for (int i = 0; i < totalSize; ++i) {
map[i] = CommonTile.NONE;
}
}
/**
*
* Performs a <strong>Deep</strong> copy of the given tilemap. The generated tile map is guaranteed to be logically
* distinct from the original, including all tiles which contain state. Whilst references may be shared, that will only
* occur for immutable types.
*
* @return
* a deep copy of the tile map
*
*/
public TileMap copy() {
TileMap newMap = new TileMap(rows, cols);
for (int i = 0; i < rows * cols; ++i) {
newMap.map[i] = this.map[i].copy();
}
return newMap;
}
/**
*
* Adds the given tile to the mapping via the x,y co-ordinate, such as when clicked in a level editor. This will automatically
* handle converting to the row/col format. Note that the x, y location is a tile id, NOT a pixel location.
* <p/>
* This does NOT throw exceptions. It is up to the client to provide sensible values. If there is a chance the value may not be sensible,
* check with {@code getRowCount() and getColumnCount() }
* <p/>
* This method, however, WILL fail if assertions are enabled, if the passed tile is {@code null}. {@code null} is NEVER a valid type.
*
* @param x
* x location on screen, resolved to tile indicator (divide by tile size)
*
* @param y
* y location on screen, resolved to tile indicator (divide by tile size)
*
* @param tile
* This does NOT throw exceptions. It is up to the client to provide sensible values. If sensible values are not provided, this method
* simply does nothing. In the editor if the user somehow selects something outside of bounds, this is probably actually fine behaviour.
*
*/
public void setTileXY(int x, int y, TileType tile) {
// Yah, it's a simple inversion of the parameters. But it gets confusing sometimes.
setTileRowCol(y, x, tile);
}
/**
*
* Erases the given tile at the given position. Erased tiles represent a logicial 'none' tile, which is effectively empty space.
* <p/>
* If the given location is out of bounds, this method does nothing.
*
* @param x
* x location NOT IN PIXELS
*
* @param y
* y location NOT IN PIXELS
*
*/
public void eraseTileXY(int x, int y) {
eraseTileRowCol(y, x);
}
/**
*
* Adds the given tile to the mapping via a row/col position. This is resolved differently from x y position. As with that method,
* this is a tile id, NOT a pixel location.
* <p/>
* This does NOT throw exceptions for out of bounds row/col. It is up to the client to provide sensible values. If sensible values are not provided, this method
* simply does nothing. In the editor if the user somehow selects something outside of bounds, this is probably actually fine behaviour.
* <p/>
* This method, however, WILL fail if assertions are enabled, if the passed tile is {@code null}. {@code null} is NEVER a valid type.
*
* @param row
* row of the tilemap
*
* @param col
* column of the tilemap
*
* @param tile
* the actual tile to place. This will be placed AS IS with NO COPYING, so it is up to the client to ensure that tiles with
* their own state are not added to multiple locations.
*
*/
public void setTileRowCol(int row, int col, TileType tile) {
assert tile != null;
if (row >= rows || row < 0) return;
if (col >= cols || row < 0) return;
int index = resolveViaRowCol(row, col);
map[index] = tile;
}
/**
*
* Erases the given tile at the given position. Erased tiles represent a logicial 'none' tile, which is effectively empty space.
* <p/>
* If the given location is out of bounds, this method does nothing.
*
* @param x
* x location NOT IN PIXELS
*
* @param y
* y location NOT IN PIXELS
*
*/
public void eraseTileRowCol(int row, int col) {
if (row >= rows || row < 0) return;
if (col >= cols || row < 0) return;
int index = resolveViaRowCol(row, col);
map[index] = CommonTile.NONE;
}
/**
*
* Returns a subset of the tiles starting from position [row1, col1] to [row2, col2]. The tiles are copied to a new array, but this
* is a shallow copy. Modifications to the contained tiles will affect them on the tile map.
* <p/>
* This is typically used when analysing a set of tiles, such as those around Bonzo, for collision.
* <p/>
* If any part of the subset is outside of bounds, it is simply ignored. If, however, row2 or col2 are less than row1/col1, an assertion
* error is produced; clients should never have this happen. This check is done AFTER cutting row2/col2 down to the maximum allowed size
* based on the size of the map.
*
* @param row1
* top-left row
*
* @param col1
* top-left col
*
* @param row2
* bottom-right row
*
* @param col2
* bottom-right col
*
* @return
* list of all tiles in the given subset, in top-left to bottom-right order
*
*/
public List<TileType> subset(int row1, int col1, int row2, int col2) {
// Cut down row2 and col2 to the minimum of the row/col size of the map. Very important otherwise too big of a column
// may cause an overlap that grabs from the next row in a totally different position.
row2 = Math.min(row2, rows);
col2 = Math.min(col2, cols);
assert row2 > row1;
assert col2 > col1;
List<TileType> sub = new ArrayList<TileType>((row2 - row1) * (col2 - col1) );
for (int i = row1; i < row2; ++i) {
int index = resolveViaRowCol(i, col1);
// We can just increment by one for inner array, saving time.
for (int j = col1; j < col2; ++j) {
sub.add(map[index + j]);
}
}
return sub;
}
/**
*
* Indicates a direction in the tilemap, intended for the {@code expand} and {@code shrink} methods
*
* @author Erika Redmark
*
*/
public enum Direction { NORTH, SOUTH, EAST, WEST }
/**
*
* Creates a new instance of the current tilemap, but with the tilemap resized in some given direction.
* The direction is basically thought of as extending or pinching the map towards that direction. It is intended to know
* where the relative locations of all the current existing tiles on the current map should be in relation to
* the new, bigger map.
* <p/>
* If the map is shrunk in some direction, it means that that amount of rows or columns will be deleted starting
* from that direction. Any tiles that were placed there will be gone in the new map.
* <p/>
* If the map is expanded in some direction, all tiles in the new rows/cols will be empty.
* <p/>
* This method returns a deep copied version of the current map, as the size of a tile map is immutable. As such,
* it is only intended to be used from the level editor.
*
* @param amount
*
* @param dir
* the direction to expand into for the new map
*
* @return
* new instance of the tile map, with a deep copy of the current map and the row/col sized changed according to the
* expansion rules.
*
*/
public TileMap resize(int amount, Direction dir) {
// Determine the new size of the map
// Direction will tell us which way to 'shift' the existing tile
int newRows = rows;
int newCols = cols;
int rowShift = 0;
int colShift = 0;
switch(dir) {
case NORTH:
rowShift = -(amount);
// Break omitted: SOUTH needs no shifting
case SOUTH:
newRows += amount;
break;
case WEST:
colShift = -(amount);
// break omnitted: EAST needs no shifting
case EAST:
newCols += amount;
break;
}
// Idea: getTileRowCol returns none for tiles out of range. So, we fill in the new map only using getTileRowAndCol, applying required
// 'shifts' to get the old map to overlay over the new map. If we try to get a position out of range, like a negative value, due to an
// expansion either NORTH or WEST, we just fill the newly created row with nones. On the contrary, for a shrinking operation, we will
// never even call getTileRowCol for certain values, hence properly meaning they aren't copied to the new map.
TileMap newMap = new TileMap(newRows, newCols);
for (int i = 0; i < newRows; ++i) {
for (int j = 0; j < newCols; ++j) {
// If out of bounds, NONE is returned. That's actually what we want, so don't worry if we shift out of reach
newMap.setTileRowCol(i, j, this.getTileRowCol(i + rowShift, j + colShift) );
}
}
return newMap;
}
/**
*
* Returns the tile at the given row/col position.
* <p/>
* If this is out of bounds, it will return {@code TileType.NONE}
*
* @param row
* @param col
*
* @return
* tile at the given row/column position
*
*/
public TileType getTileRowCol(int row, int col) {
if (row < 0 || row >= rows) return CommonTile.NONE;
if (col < 0 || col >= cols) return CommonTile.NONE;
return map[resolveViaRowCol(row, col)];
}
/**
*
* Returns the tile at the given x/y position. Not a pixel location.
* <p/>
* If this is out of bounds, it will return {@code TileType.NONE}
*
* @param x
* @param y
*
* @return
* tile at the given x/y position
*
*/
public TileType getTileXY(int x, int y) {
if (y < 0 || y >= rows) return CommonTile.NONE;
if (x < 0 || x >= cols) return CommonTile.NONE;
return map[resolveViaRowCol(y, x)];
}
/**
*
* Returns the tile at the given pixel location. Tiles are always 20x20. The pixel location should be normalised such that 0,0 is the top-left
* of the very first tile.
* <p/>
* If this is out of bounds, it will return {@code TileType.NONE}
*
* @param xPixel
* @param yPixel
*
* @return
* tile at the given x/y pixel location
*
*/
public TileType getTileXYPixel(int xPixel, int yPixel) {
final int row = yPixel / GameConstants.TILE_SIZE_Y;
final int col = xPixel / GameConstants.TILE_SIZE_X;
if (row < 0 || row >= rows) return CommonTile.NONE;
if (col < 0 || col >= cols) return CommonTile.NONE;
return map[resolveViaRowCol(
yPixel / GameConstants.TILE_SIZE_Y,
xPixel / GameConstants.TILE_SIZE_X)
];
}
/**
*
* For every tile in the map, it's state, if it has any, is reset. This should be called when a tile map is
* loaded with relevant data, such as loading a tile map for a level screen for the first time, and as well
* when bonzo moves off the screen (or for the level editor when it decides to invalidate the current tiles).
*
*/
public void resetTiles() {
// Use array indexing as we need the odd/eveness for setting animation steps.
// We need a little trick here: we invert the logic on odd ROWS. This is because
// we need two 'adjacent in memory' tiles to animate the same, because logically the next one is
// on the next 'row' and would otherwise not be staggered with the animation cell above it
int check = 1;
for (int i = 0; i < map.length; ++i) {
// did we hit the next row? invert the logic to stagger. This actually doesn't work for odd-sized
// tile maps, but this almost always used with the main level and if it doesn't stagger in templates,
// it really doesn't matter.
if ( (i % cols) == 0) check = (check == 0) ? 1 : 0;
map[i].reset( (i % 2) == check);
}
}
public int getRowCount() { return rows; }
public int getColumnCount() { return cols; }
private int resolveViaRowCol(int row, int col) {
return (row * this.cols) + col;
}
/**
*
* Paints the entire tilemap to the graphics context starting at the 0, 0 point (use affinity transforms before passing to change), and
* draws each tile at the given row/column dimensions this map was created with.
*
* @param g2d
* graphics context to draw to
*
* @param rsrc
* the world resource for drawing the tiles. Tile graphics are based on internal id synced with the given graphics object
*
*/
public void paint(Graphics2D g2d, WorldResource rsrc) {
for (int i = 0; i < map.length; ++i) {
map[i].paint(g2d,
(i % cols) * GameConstants.TILE_SIZE_X,
(i / cols) * GameConstants.TILE_SIZE_Y,
rsrc);
}
}
/**
*
* Updates all tiles in the map.
*
*/
public void update() {
for (TileType t : map) t.update();
}
/**
*
* Returns the backing array of tiles. Should only truly be used if an external algorithm requires
* iterating over all tiles in the map.
*
* @return
* backing array of tiles in the map. Intended for iteration only
*
*/
public TileType[] internalMap() {
return map;
}
@Override public boolean equals(Object o) {
if (o == this) return true;
if ( !(o instanceof TileMap) ) return false;
TileMap other = (TileMap) o;
// check size
if ( other.rows != this.rows
|| other.cols != this.cols) {
return false;
}
// Now check underlying tiles for iteration order
// Not the size check is NOT optional. Two differently sized maps could theoretically have the same tile type iteration order.
TileType[] myTiles = this.internalMap();
TileType[] otherTiles = other.internalMap();
// If row and col check checked out the lengths MUST be the same
assert myTiles.length == otherTiles.length : "Lengths should be identical if row/col check succeeded";
for (int i = 0; i < myTiles.length; ++i) {
if (!(myTiles[i].equals(otherTiles[i]) ) ) {
return false;
}
}
return true;
}
@Override public int hashCode() {
int result = 17;
result += result * 31 + rows;
result += result * 31 + cols;
for (int i = 0; i < map.length; ++i) {
result += map[i].hashCode();
}
return result;
}
private int rows;
private int cols;
private TileType[] map;
}